<!DOCTYPE html>
<html lang="zh-cmn-Hans">
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <title>HTML5 Canvas 3D球形文字标签云动画DEMO演示</title>
  </head>
  <style>
    * {
      margin: 0;
      padding: 0;
    }
  </style>
  <body>
    <div style="width: 100vw;height: 100vh;"><canvas id="canvasOne"></canvas></div>

    <script type="text/javascript">
      //生成从minNum到maxNum的随机数
      function randomNum(minNum, maxNum) {
        switch (arguments.length) {
          case 1:
            return parseInt(Math.random() * minNum + 1, 10);
            break;
          case 2:
            return parseInt(Math.random() * (maxNum - minNum + 1) + minNum, 10);
            break;
          default:
            return 0;
            break;
        }
      }

      window.addEventListener('load', windowLoadHandler, false);
      // 是否随机颜色
      var idRandomTextColor = false;
      // 文字颜色 220,194,135
      var fixedTextColor = 'rgba(220,194,135,';
      // 文字随机的大小范围，数组样式，如果不变大小，设置前后一样。
      var fontRandomText = [5, 35];
      function getRandomColor() {
        r = randomNum(0, 255);
        g = randomNum(0, 255);
        b = randomNum(0, 255);
        return 'rgba(' + r + ',' + g + ',' + b + ',';
      }
      // 半径
      var sphereRad = 500;
      var radius_sp = 1;
      var opt_display_dots = false;

      var unicodeFlakes = '卐卍唵嘛卐卍呢叭卐卍咪吽'.split('');

      function windowLoadHandler() {
        canvasApp();
      }
      function canvasApp() {
        var theCanvas = document.getElementById('canvasOne');

        var parentNodeStyle = window.getComputedStyle(theCanvas.parentNode, null);
        theCanvas.width = parseInt(parentNodeStyle.getPropertyValue('width'));
        theCanvas.height = parseInt(parentNodeStyle.getPropertyValue('height'));

        var context = theCanvas.getContext('2d');

        var displayWidth;
        var displayHeight;
        var timer;
        var wait;
        var count;
        var numToAddEachFrame;
        var particleList;
        var recycleBin;
        var particleAlpha;
        var fLen;
        var m;
        var projCenterX;
        var projCenterY;
        var zMax;
        var turnAngle;
        var turnSpeed;
        var sphereCenterX, sphereCenterY, sphereCenterZ;
        var particleRad;
        var zeroAlphaDepth;
        var randAccelX, randAccelY, randAccelZ;
        var gravity;
        var rgbString;
        // 我们正在全局定义屏幕更新函数中使用的许多变量，以便不必重新定义每个帧
        var p;
        var outsideTest;
        var nextParticle;
        var sinAngle;
        var cosAngle;
        var rotX, rotZ;
        var depthAlphaFactor;
        var i;
        var theta, phi;
        var x0, y0, z0;

        init();

        window.addEventListener('resize', function() {
          clearInterval(timer);
          theCanvas.width = parseInt(parentNodeStyle.getPropertyValue('width'));
          theCanvas.height = parseInt(parentNodeStyle.getPropertyValue('height'));
          context.fillStyle = '#000000';
          context.beginPath();
          context.fillRect(0, 0, theCanvas.width, theCanvas.height);
          context.closePath();
          init();
        });
        // INITIALLI
        function init() {
          wait = 1;
          count = wait - 1;
          numToAddEachFrame = 4;

          particleAlpha = 1; //maximum alpha

          displayWidth = theCanvas.width;
          displayHeight = theCanvas.height;
          // 表示从观看者到Z＝0深度的距离
          fLen = 500;
          // 投影中心坐标集原点定位
          projCenterX = displayWidth / 2;
          projCenterY = displayHeight / 2;

          //如果z坐标太大（这意味着它们非常接近观察者），我们就不会绘制坐标。
          zMax = fLen - 2;

          particleList = {};
          recycleBin = {};

          // 随机加速因子引起随机运动
          randAccelX = 0.1;
          randAccelY = 0.1;
          randAccelZ = 0.1;

          // 0 粒子四周发散， <0 则向上飘散 ， >0 则向下飘散
          gravity = -0;

          particleRad = 2.5;

          sphereCenterX = 0;
          sphereCenterY = 0;
          sphereCenterZ = -3 - sphereRad;

          //alpha值将随着粒子进一步向后移动而减小，导致基于深度的变暗：
          zeroAlphaDepth = -750;

          turnSpeed = (2 * Math.PI) / 300; //球体将以这个速度旋转
          turnAngle = 0; //initial angle

          onTimer(); 
        }

        function onTimer() {
          // 如果足够的时间过去，我们将添加新的粒子。
          count++;
          if (count >= wait) {
            count = 0;
            for (i = 0; i < numToAddEachFrame; i++) {
              theta = Math.random() * 2 * Math.PI;
              phi = Math.acos(Math.random() * 2 - 1);
              x0 = sphereRad * Math.sin(phi) * Math.cos(theta);
              y0 = sphereRad * Math.sin(phi) * Math.sin(theta);
              z0 = sphereRad * Math.cos(phi);

              //我们使用AdQuoStand函数来添加新粒子。参数设置了位置和速度分量。 注意，速度参数将导致粒子最初从球形中心向外飞行(之后)。它变得松散了。
              var p = addParticle(x0, sphereCenterY + y0, sphereCenterZ + z0, 0.002 * x0, 0.002 * y0, 0.002 * z0);

              //we set some "envelope" parameters which will control the evolving alpha of the particles.
              p.attack = 50;
              p.hold = 50;
              p.decay = 100;
              p.initValue = 0;
              p.holdValue = particleAlpha;
              p.lastValue = 0;

              //the particle will be stuck in one place until this time has elapsed:
              p.stuckTime = 90 + Math.random() * 20;

              p.accelX = 0;
              p.accelY = gravity;
              p.accelZ = 0;
            }
          }

          //update viewing angle
          turnAngle = (turnAngle + turnSpeed) % (2 * Math.PI);
          sinAngle = Math.sin(turnAngle);
          cosAngle = Math.cos(turnAngle);

          //background fill
          context.fillStyle = '#000000';
          context.fillRect(0, 0, displayWidth, displayHeight);

          //update and draw particles
          p = particleList.first;
          while (p != null) {
            //在列表改变之前记录下一个粒子
            nextParticle = p.next;

            //update age
            p.age++;

            //如果粒子经过它的“滞留”时间，它将开始移动
            if (p.age > p.stuckTime) {
              p.velX += p.accelX + randAccelX * (Math.random() * 2 - 1);
              p.velY += p.accelY + randAccelY * (Math.random() * 2 - 1);
              p.velZ += p.accelZ + randAccelZ * (Math.random() * 2 - 1);

              p.x += p.velX;
              p.y += p.velY;
              p.z += p.velZ;
            }

            /*
              我们在这里做两件事来计算显示坐标。
              整个显示器围绕垂直轴旋转，因此我们首先计算用于
              X和Z（但Y坐标不会改变）。
              然后，我们将新的坐标（rotX，y，rotZ）投影到2D视图平面上
            */
            rotX = cosAngle * p.x + sinAngle * (p.z - sphereCenterZ);
            rotZ = -sinAngle * p.x + cosAngle * (p.z - sphereCenterZ) + sphereCenterZ;
            m = (radius_sp * fLen) / (fLen - rotZ);
            p.projX = rotX * m + projCenterX;
            p.projY = p.y * m + projCenterY;

            //update alpha according to envelope parameters. 根据包络参数更新Alpha。
            if (p.age < p.attack + p.hold + p.decay) {
              if (p.age < p.attack) {
                p.alpha = ((p.holdValue - p.initValue) / p.attack) * p.age + p.initValue;
              } else if (p.age < p.attack + p.hold) {
                p.alpha = p.holdValue;
              } else if (p.age < p.attack + p.hold + p.decay) {
                p.alpha = ((p.lastValue - p.holdValue) / p.decay) * (p.age - p.attack - p.hold) + p.holdValue;
              }
            } else {
              p.dead = true;
            }

            //看看粒子是否仍在可视范围内。
            if (p.projX > displayWidth || p.projX < 0 || p.projY < 0 || p.projY > displayHeight || rotZ > zMax) {
              outsideTest = true;
            } else {
              outsideTest = false;
            }

            if (outsideTest || p.dead) {
              recycle(p);
            } else {
              //depth-dependent darkening
              depthAlphaFactor = 1 - rotZ / zeroAlphaDepth;
              depthAlphaFactor = depthAlphaFactor > 1 ? 1 : depthAlphaFactor < 0 ? 0 : depthAlphaFactor;

              context.fillStyle = p.color + depthAlphaFactor * p.alpha + ')';
              /*ADD TEXT function!*/
              /*ADD TEXT function!*/
              /*ADD TEXT function!*/
              /*ADD TEXT function!*/
              // 设置字体大小
              context.font = p.font;
              context.fillText(p.flake, p.projX, p.projY);
              /*ADD TEXT function!*/
              /*ADD TEXT function!*/
              /*ADD TEXT function!*/
              /*ADD TEXT function!*/
              //draw
              context.beginPath();
              if (opt_display_dots) {
                context.arc(p.projX, p.projY, m * particleRad, 0, 2 * Math.PI, false);
              }
              context.closePath();
              context.fill();
            }

            p = nextParticle;
          }
          requestAnimationFrame(onTimer);
        }

        function addParticle(x0, y0, z0, vx0, vy0, vz0) {
          var newParticle;
          var color;

          //check recycle bin for available drop:
          if (recycleBin.first != null) {
            newParticle = recycleBin.first;
            //remove from bin
            if (newParticle.next != null) {
              recycleBin.first = newParticle.next;
              newParticle.next.prev = null;
            } else {
              recycleBin.first = null;
            }
          }
          //if the recycle bin is empty, create a new particle (a new ampty object):
          else {
            newParticle = {};
          }

          //add to beginning of particle list
          if (particleList.first == null) {
            particleList.first = newParticle;
            newParticle.prev = null;
            newParticle.next = null;
          } else {
            newParticle.next = particleList.first;
            particleList.first.prev = newParticle;
            particleList.first = newParticle;
            newParticle.prev = null;
          }

          //initialize
          newParticle.x = x0;
          newParticle.y = y0;
          newParticle.z = z0;
          newParticle.velX = vx0;
          newParticle.velY = vy0;
          newParticle.velZ = vz0;
          newParticle.age = 0;
          newParticle.dead = false;

          newParticle.flake = unicodeFlakes[Math.floor(Math.random() * unicodeFlakes.length)];
          if (Math.random() < 0.5) {
            newParticle.right = true;
          } else {
            newParticle.right = false;
          }
          newParticle.font = randomNum(fontRandomText[0], fontRandomText[1]) + 'px Arial';
          newParticle.color = idRandomTextColor ? getRandomColor() : fixedTextColor;
          return newParticle;
        }

        function recycle(p) {
          //remove from particleList
          if (particleList.first == p) {
            if (p.next != null) {
              p.next.prev = null;
              particleList.first = p.next;
            } else {
              particleList.first = null;
            }
          } else {
            if (p.next == null) {
              p.prev.next = null;
            } else {
              p.prev.next = p.next;
              p.next.prev = p.prev;
            }
          }
          //add to recycle bin
          if (recycleBin.first == null) {
            recycleBin.first = p;
            p.prev = null;
            p.next = null;
          } else {
            p.next = recycleBin.first;
            recycleBin.first.prev = p;
            recycleBin.first = p;
            p.prev = null;
          }
        }
      }
    </script>
  </body>
</html>
